Introduction to Python — CS424

hello?

Much should be familiar, so let's focus on differences:

1) Interpreted with REPL.

Makes for a rather boring "hello world!":

>>> 'hello.'

But great for experimenting:

>>> dir('hello.')
'upper' looks interesting ...
>>> dir('hello'.upper)
Maybe '__doc__' will help ...
>>> print 'hello'.upper.__doc__
...
>>> 'hello'.upper()
OK, so we cheated a bit: had to know about dir() and print. How would we learn more about dir (and why won't that work for print)?

Worth noting:

Basic Types

All the usual types are available: booleans, integers, reals, strings; with dynamic typing:

>>> x = 'three'
>>> x = 3
Integers are “infinite”:
>>> for shift in range(100): print shift, 1 << shift
so don't count on wrap-around behavior.

There is no byte/character type, just a one element string.

Control Flow

As we've just seen, for loops are available, with break and continue. for may also have an else (executed if the loop is not broken). The if, elif, else construct is also present, but not 'switch' or 'while'.

Statement groups within the body of a for loop or an if branch are set off by indention, even in the REPL:

>>> for x in ['cat', 'dog', 'mouse']:
... if x.upper() == 'DOG':
  File "", line 2
    if x.upper() == 'DOG':
     ^
IndentationError: expected an indented block
>>> for x in ['cat', 'dog', 'mouse']:
...  if x.upper() == 'DOG':
...  print x + ' grew up to be a big dog!'
  File "", line 3
    print x + ' grew up to be a big dog!'
        ^
IndentationError: expected an indented block
>>> for x in ['cat', 'dog', 'mouse']:
...  if x.upper() == 'DOG':
...   print x + ' grew up to be a big dog!'
... 
dog grew up to be a big dog!

Container types

Python has three basic container types: dictionaries, list and tuples.

List are fairly self-evident: they impose an ordering on a heterogeneous collection of objects, including other lists. A given element of a list may be referenced via it's 0-based index, and list objects support a number of methods (as usual, try dir).

>>> l = ['a', 2, [3.4, 5.6]]
>>> l[0]
'a'
>>> l[-1]
[3.3999999999999999, 5.5999999999999996]
>>> len(l)
3
>>> l[:2]
['a', 2]
>>> l[::-1]
[[3.3999999999999999, 5.5999999999999996], 2, 'a']
>>> l.append('moo')
>>> l
['a', 2, [3.3999999999999999, 5.5999999999999996], 'moo']
>>> l[2][1]
5.5999999999999996
>>> l.pop(2)
[3.3999999999999999, 5.5999999999999996]
>>> l
['a', 2, 'moo']
>>> ll = l + l
>>> ll
['a', 2, 'moo', 'a', 2, 'moo']
Tuples are akin to lists, but are immutable and are constructed using ( ).
>>> t = ('a', 2, 'moo')
>>> l
['a', 2, 'moo']
>>> t
('a', 2, 'moo')
>>> l[1] = 'cow'
>>> l
['a', 'cow', 'moo']
>>> t[1] = 'cow'
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'tuple' object does not support item assignment
Dictionaries are extremely useful mapping containers:
>>> d = {'perl': 'good', 'ruby': 'better', 'python': 'best'}
>>> d['python']
'best'
>>> for k in d: print k, d[k]
... 
python best
ruby better
perl good
>>> len(d)
3
>>> for k in ['ada', 'python', 'tex']: print k, d.get(k, '???')
... 
ada ???
python best
tex ???
You can dynamically add new keys to the dictionary:
>>> rolls = {}
>>> for x in range(10): rolls[randint(1,6)] = 1
... 
>>> rolls
{1: 1, 2: 1, 3: 1, 5: 1, 6: 1}
And easily test for the presence of a key:
>>> [ x for x in range(1,7) if x not in rolls ]
[4]
Interesting list construction, no? A list comprehension. “One liner” for loops, and then some: conditionally apply processing to the elements of nested lists of sequences.
>>> powers = [[x**y for y in range(0, 5)] for x in range(0, 101)]
>>> powers[2][4]
16
A little more complex:
>>> factors = [[y for y in range(1,x) if x and not x%y ] for x in range(1000)]
>>> [x for x in range(1000) if x == sum(factors[x])]

Functions

Basics:

def factorial(n):
    if n == 0: return 1
    else: return n*factorial(n-1)
As usual, code blocks are set off by indentation. And, as usual, functions are objects:
>>> dir(factorial)
['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__get__',
 '__getattribute__', '__hash__', '__init__', '__module__', '__name__',
 '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__',
 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc',
 'func_globals', 'func_name']
>>> dir(factorial.func_code)
['__class__', '__cmp__', '__delattr__', '__doc__', '__getattribute__',
 '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
 '__setattr__', '__str__', 'co_argcount', 'co_cellvars', 'co_code',
 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars',
 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
Power users can have a field day with these.

No return required:

def hw():
    print 'hola.'

>>> print hw()
hola.
None
But many values allowed:
def mv(): return 1, 2.3, 'hi'

>>> r = mv()
>>> r
(1, 2.2999999999999998, 'hi')
>>> r0, r1, r2 = mv()
>>> print r0, r1, r2
1 2.3 hi
And what about that nifty __doc__ stuff?
def hw():
    'You say goodbye and I say hello'
    return 'hello'

>>> hw()
'hello'
>>> print hw.__doc__
You say goodbye and I say hello

An argument list can have a fairly complex structure:

def args(a, *b, **c):
    print 'a: ', a
    print 'b: ', b
    print 'c: ', c

>>> args(123, 4, 5, 6, cat='dog')
123
(4, 5, 6)
{'cat': 'dog'}
Possible to have positional, variadic and “keyword” arguments. Positionals can be named and have defaults. We'll only comment on a few features here, see the usual references for details.

Named arguments are useful for “simulating” polymorphism:

def pt(x=0, y=0): return (x, y)

>>> pt()
(0, 0)
>>>pt(1)
(1, 0)
>>> pt(y=2)
(0, 2)
>>> pt(x=2)
(2, 0)
>>> pt(3, 4)
(3, 4)
>>>
The * and ** syntax can also be used for invocations:
>>> t = (111, 222)
>>> pt(t)
((111, 222), 0)
>>> pt(*t)
(111, 222)
>>> d = {'y': -1, 'x': 1}
>>> pt(d)
({'y': -1, 'x': 1}, 0)
>>> pt(*d)
('y', 'x')
>>> pt(**d)
(1, -1)
For you Haskell/ML fans:
def tx(x):
    def f(n): return x*n
    return f

>>> t3 = tx(3)
>>> t13 = tx(13)
>>> t3(11)
33
>>> t13(11)
143
Now that we are dealing with larger amounts of code, we should note that python can be invoked with a script file on the command line, in which case the commands executed are taken from the script. (One can also use #!/usr/bin/env python as the first line and chmod the script to make it directly executable.)

Exceptions

Python has an exception facility for dealing with ... exceptions! Three parts: raise, try, and except.
def grouchy(x): raise Exception('I don\'t like %s!'%repr(x))

>>> grouchy(3)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 1, in grouchy
Exception: I don't like 3!
By default, an exception generates a trace back, an Exception description and returns control to the interpreter or exits a script. This behavior can be changed by the use of tryexcept:
>>> for x in ['peas', 'carrots']:
...     try: grouchy(x)
...     except Exception, e: print e
... else: print 'all done'
... 
I don't like 'peas'!
I don't like 'carrots'!
all done
>>> for x in ['peas', 'carrots']: grouchy(x)
... 
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 1, in grouchy
Exception: I don't like 'peas'!
A full block of code can follow the :. If not altered by the except code block, control will flow to the next statement after the tryexcept when an exception is “caught”.

Exceptions are the general mechanism used by Python for almost all problems:

>>> 1/0
Traceback (most recent call last):
  File "", line 1, in 
ZeroDivisionError: integer division or modulo by zero
>>> open('NoFile')
Traceback (most recent call last):
  File "", line 1, in 
IOError: [Errno 2] No such file or directory: 'NoFile'
>>> 
except can be targeted (as well as nested and chained):
>>> try: open('NoFile')
... except IOError: 'no biggie.'
... 
'no biggie.'
>>> try: 1/0
... except IOError: 'no biggie.'
... 
Traceback (most recent call last):
  File "", line 1, in 
ZeroDivisionError: integer division or modulo by zero
This mechanism, of course, can be used for general flow control — not just exceptional events.
def boredP(x, depth=0):
    if depth > 13: raise Exception('I\'m bored!')
    if x: boredP(x-1, depth+1)

>>> try: boredP(3)
... except: 'got bored'
... 
>>> try: boredP(33)
... except: 'got bored'
... 
'got bored'
Note: because exceptions are so general, using untargeted excepts may be a bad idea in that they can mask important problems.

Debugging

The default debugger is pdb. It offers a gdb-like user interface: break, next, step, where, etc. Type help when in the debugger for a full list. Anything not recognized as a debugger command is assumed to be a normal Python statement and is executed in the current context.
>>> import pdb
>>> pdb.runcall(ff, 7)
> /home/carriero/python/cs424/intro2/ff.py(2)ff()
-> if 0 == n: return 1
(Pdb) step
> /home/carriero/python/cs424/intro2/ff.py(3)ff()
-> return n*ff(n-1)
(Pdb) 
--Call--
> /home/carriero/python/cs424/intro2/ff.py(1)ff()
-> def ff(n):
(Pdb) 
> /home/carriero/python/cs424/intro2/ff.py(2)ff()
-> if 0 == n: return 1
(Pdb) 
> /home/carriero/python/cs424/intro2/ff.py(3)ff()
-> return n*ff(n-1)
(Pdb) 
--Call--
> /home/carriero/python/cs424/intro2/ff.py(1)ff()
-> def ff(n):
(Pdb) where
  /home/carriero/myInstalls/lib/python2.5/bdb.py(404)runcall()
-> res = func(*args, **kwds)
  /home/carriero/python/cs424/intro2/ff.py(3)ff()
-> return n*ff(n-1)
  /home/carriero/python/cs424/intro2/ff.py(3)ff()
-> return n*ff(n-1)
> /home/carriero/python/cs424/intro2/ff.py(1)ff()
-> def ff(n):
(Pdb) print n
5
(Pdb) return
--Return--
> /home/carriero/python/cs424/intro2/ff.py(3)ff()->120
-> return n*ff(n-1)
(Pdb) 
--Return--
> /home/carriero/python/cs424/intro2/ff.py(3)ff()->720
-> return n*ff(n-1)
(Pdb) return
--Return--
> /home/carriero/python/cs424/intro2/ff.py(3)ff()->5040
-> return n*ff(n-1)
(Pdb) 
5040
>>>
The debugger may be run, as just shown, within an interactive session, or it may be used to run a script file. In the latter case (or in an interactive session but when debugging code read from a file, say with import or execfile), the debugger plays nicely with emacs.

It is worth investing a little time becoming comfortable with the debugger. The pdb module documentation at the Python web site is good, but no substitute for some hands-on experimenting.

Classes

As we have seen, Python is “thoroughly” object oriented. Note: not “pure” or “rigorous”, just thorough.

One way to think of a Python class instance is as a kind of container:

>>> class Struct: pass
... 
>>> s = Struct()
>>> s.a = 1
>>> s.b = 2.3
>>> s.c = ['a', 'b', 'c']
>>> s
<__main__.Struct instance at 0xb7c707ac>
>>> dir(s)
['__doc__', '__module__', 'a', 'b', 'c']
>>> s.b
2.2999999999999998
>>> 
By convention, interesting things happen with certain bits of the contents:
class C:
    'C is for "container".'
    def __init__(self, forMe=None):
        print 'Instantiating a C'
        if forMe: self.forMe = forMe

>>> C.__doc__
'C is for "container".'
>>> c0 = C()
Instantiating a C
>>> c0.__doc__
'C is for "container".'
>>> dir(c0)
['__doc__', '__init__', '__module__']
But the instance is still a container ...
>>> c0.foo = 123
>>> dir(c0)
['__doc__', '__init__', '__module__', 'foo']
>>> c0.foo
123
>>> c1 = C('hi')
Instantiating a C
>>> dir(c1)
['__doc__', '__init__', '__module__', 'forMe']
>>> c1.forMe
'hi'
>>> c1.foo = 321
>>> dir(c1)
['__doc__', '__init__', '__module__', 'foo', 'forMe']
>>> c1.foo
321
It should be clear that __init__ is a bit of “special” content that corresponds to a constructor. There are others:
class C:
    'C is for "container".'
    def __init__(self, forMe=None):
        print 'Instantiating a C'
        if forMe: self.mine = forMe
    
    def __str__(self):
        try:    return repr(self.mine) + ' is all mine.'
        except: return 'I\'m empty'
    
    def __getitem__(self, tag): return 'I sure wish I had a '+repr(tag)
    
    def __setitem__(self, tag, val):
        print 'No room for a %s in the %s box'%(repr(val), repr(tag))
>>> c0 = C()
Instantiating a C
>>> c1 = C('Foo')
Instantiating a C
>>> print c0
I'm empty
>>> print c1
'Foo' is all mine.
>>> c1[3] = 'cat'
No room for a 'cat' in the 3 box
>>> c1[3]
'I sure wish I had a 3'
A more traditional example:
class S:
    'S is for "stack".'
    
    def __init__(self, data=[]):
        self.data = data[::]  # = data would be a mistake.
  
    def push(self, v): self.data.append(v)
   
    def pop(self): return self.data.pop(-1)
   
    def __iter__(self):
        class x:
            def __init__(self, l): self.l = l[::]
            def next(self):
                if not self.l: raise StopIteration
                return self.l.pop(0)
        return x(self.data)
>>> s = S()
>>> s.push('cat')
>>> s.push('mouse')
>>> s.push('dog')
>>> for x in s:
...  for y in s: print x, y
... 
cat cat
cat mouse
cat dog
mouse cat
mouse mouse
mouse dog
dog cat
dog mouse
dog dog
Subclassing, of course, is also “in there”:
from random import randint
class RS(S):
     'R is for "random".'
  
     def __init__(self, data=[]):
         S.__init__(self, data)
  
     def push(self, v): self.data.insert(randint(0, len(self.data)), v)

>>> s = S()
>>> r = RS()
>>> for x in xrange(13):
...  s.push(x)
...  r.push(x)
... 
So much for encpasulation:
>>> s.data
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
>>> r.data
[8, 9, 10, 7, 4, 5, 0, 6, 12, 11, 2, 3, 1]
The class RS does inherit the iterator and the pop method:
>>> [x for x in r]
[8, 9, 10, 7, 4, 5, 0, 6, 12, 11, 2, 3, 1]
>>> r.pop()
1

Modules

Python core functionality is rich, but there is much more to be had via the module mechanism. Two of the more important are os and sys.
>>> import sys
>>> sys.version
'2.5 (r25:51908, Nov 20 2006, 21:33:12) \n[GCC 4.1.1 20061011 (Red Hat 4.1.1-30)]'
>>> sys.argv
['']
>>> sys.stdout.write('hola.')
hola.>>> sys.stdout.write('hola.\n')
hola.
import is not the only way to access a module.
>>> from os import getcwd, popen as pOp
>>> getcwd()
'/home/carriero/python/cs424/intro2'
>>> for l in pOp('ls *py'): print l[:-1]
... 
ff.py
menu.py
If you are developing a module, note that you can reload, but this does not always do what you want — you may want to use execfile during the main debugging phase.

Also see the module imp.

And that's just the beginning... .

The History of every major Galactic Civilization tends to pass through three distinct and recognizable phases, those of Survival, Inquiry and Sophistication, otherwise known as the How, Why and Where phases. For instance, the first phase is characterized by the question ''How can we eat?'', the second by the question ''Why do we eat?'' and the third by the question, ''Where shall we have lunch?''
We can advance the cause of civilization in 42 lines of code.

Resources

Lot's of useful information at the python web site.

One good book (there are many): David Beazley, Python Essential Reference, New Riders.